# Copyright (c) 2018 Presto Labs Pte. Ltd.
# Author: inkyu

import datetime
import logging
import re
import sys
import time
from datetime import timedelta
from enum import Enum

import tabulate

from coin.strategy.tool.util import find_log_pos_by_timestamp
from coin.base.datetime_util import to_datetime


class ParserState(Enum):
  READY = 0
  HAS_DATETIME = 1
  HAS_BALANCE = 2
  DONE = 3


def format_timedelta_long(time_delta):
  if time_delta is None:
    return ''

  ts = time_delta.total_seconds()
  h = int(ts / 3600)
  m = int(ts / 60) % 60
  s = int(ts % 60)
  return '%02d:%02d:%02d' % (h, m, s)


def format_timedelta_short(time_delta):
  if time_delta is None:
    return ''
  parsed = int(time_delta.total_seconds())
  return '%.2fH' % (parsed / (60.0 * 60.0))


def format_time_marked(time_marked):
  if time_marked is None:
    return ''
  return datetime.datetime.strftime(time_marked, "%m-%d %H:%M")


def datetime_diff_or_none(dt2, dt1):
  if dt2 is None or dt1 is None:
    return None
  return dt2 - dt1


class LogParser(object):
  def __init__(self,
               name,
               time_current,
               use_fill_collector=False,
               balance_marker='BTC',
               position_marker='USD',
               flexibility=timedelta(minutes=10)):
    self._time_current = time_current
    self._time_mark = (time_current - flexibility).replace(hour=0,
                                                           minute=0,
                                                           second=0,
                                                           microsecond=0)
    self._time_4h = time_current - timedelta(hours=4)
    self._time_24h = time_current - timedelta(hours=24)
    self._time_begin = time_current - timedelta(days=10000)
    self._time_last_fills = {}

    self._name = name
    self._time = None
    self._time_marked = None
    self._state = ParserState.READY
    self._use_fill_collector = use_fill_collector
    self._balance_marker = balance_marker
    self._position_marker = position_marker

    self._position_last = None
    self._position_last_1 = None
    self._position_last_2 = None

    self._value_last = None
    self._value_last_1 = None
    self._value_last_2 = None
    self._value_mark = None
    self._value_4h = None
    self._value_24h = None
    self._turnover_mark = 0
    self._turnover_4h = 0
    self._turnover_24h = 0

  def handle_price(self, line: str):
    if line.startswith('*'):
      splitted = line.split()
      if len(splitted) == 5 and splitted[1] == self._balance_marker:
        return [float(splitted[2]), float(splitted[3]), float(splitted[4])]
      elif len(splitted) == 3 and splitted[1] == self._balance_marker:
        return [float(splitted[2]), 0., float(splitted[2])]

  def handle_position(self, line: str):
    if line.startswith('*'):
      splitted = line.split()
      if len(splitted) == 5 and splitted[1] == self._position_marker:
        return [float(splitted[2]), float(splitted[3]), float(splitted[4])]
      elif len(splitted) == 4 and splitted[1] == self._position_marker:
        return [float(splitted[2]), 0., float(splitted[2])]

  def detect_datetime(self, line: str):
    matches = re.search('^\* (\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}.\d{6})\n$', line)
    if matches is None:
      return None
    marker = datetime.datetime.strptime("%s %s" % (matches.group(1), matches.group(2)),
                                        "%Y-%m-%d %H:%M:%S.%f")
    return marker

  def update_values(self, value, value_1, value_2):
    if value > 2.5:
      print('"%s"\t%f' % (self._time, value))
    if self._value_mark is None or self._time < self._time_mark:
      self._value_mark = value
      self._time_marked = self._time
    if self._value_4h is None or self._time < self._time_4h:
      self._value_4h = value
    if self._value_24h is None or self._time < self._time_24h:
      self._value_24h = value
    self._value_last = value
    self._value_last_1 = value_1
    self._value_last_2 = value_2

  def update_positions(self, position, position_1, position_2):
    self._position_last = position
    self._position_last_1 = position_1
    self._position_last_2 = position_2

  def add_turnover(self, time, turnover):
    if time > self._time_mark:
      self._turnover_mark += turnover
    if time > self._time_4h:
      self._turnover_4h += turnover
    if time > self._time_24h:
      self._turnover_24h += turnover

  def handle_fill_dump(self, line: str):
    comma_separated = line.split(',')
    if len(comma_separated) == 13 and comma_separated[0] == '"FILL_DUMP"':
      timestamp = to_datetime(int(comma_separated[1]))
      if timestamp > self._time_current:
        self._state == ParserState.DONE
        return False
      exchange_id = int(comma_separated[3])
      _side_sign = comma_separated[4]
      _fill_price = float(comma_separated[5])
      fill_qty = float(comma_separated[6])
      self._time_last_fills[exchange_id] = timestamp

      self.add_turnover(timestamp, abs(fill_qty))
      return True

    elif len(comma_separated) == 9 and comma_separated[0] == '"FILL_DUMP"':
      timestamp = to_datetime(int(comma_separated[1]))
      if timestamp > self._time_current:
        self._state == ParserState.DONE
        return False
      exchange_id = 2
      _side_sign = comma_separated[3]
      _fill_price = float(comma_separated[4])
      fill_qty = float(comma_separated[5]) * 100
      self._time_last_fills[exchange_id] = timestamp

      self.add_turnover(timestamp, abs(fill_qty))
      return True

    elif len(comma_separated) == 12 and comma_separated[0] == '"FILL_DUMP"':
      # Bitfinex: we must consider this
      timestamp = to_datetime(int(comma_separated[1]))
      if timestamp > self._time_current:
        self._state == ParserState.DONE
        return False
      exchange_id = 2
      _side_sign = comma_separated[5]
      fill_price = float(comma_separated[6])
      fill_qty = float(comma_separated[7])
      self._time_last_fills[exchange_id] = timestamp

      self.add_turnover(timestamp, abs(fill_price * fill_qty))
      return True

    return False

  def handle_fill_collector(self, line: str):
    timestamp_format = '%Y-%m-%d %H:%M:%S,%f'  # Fucking european style
    result = None
    splitted = line.split()
    fill_qty = None
    timestamp = None
    exchange_id = None

    # DEBUG 2018-03-16 17:35:21,280 Bitmex] FILL XBTUSD
    #   SELL 35000.000000@8552.0
    if (len(splitted) == 8 and splitted[0] in ('DEBUG', 'INFO') and splitted[3] == 'Bitmex]'
        and splitted[4] == 'FILL'):
      exchange_id = 2
      timestamp = datetime.datetime.strptime("%s %s" % (splitted[1], splitted[2]), timestamp_format)

      fill_qty_s, fill_price_s = splitted[7].split('@')
      fill_qty = float(fill_qty_s)
      _fill_price = float(fill_price_s)
      result = True
    # INFO 2018-02-26 10:43:40,355 SoftArbStrategy] [1] FILL
    #   OkexFuturesOrderSide.BUY_OPEN 1.000000 @ 9549.8500
    if (len(splitted) == 10 and splitted[0] == 'INFO' and splitted[3] == 'SoftArbStrategy]'
        and splitted[8] == '@'):
      exchange_id = 1
      timestamp = datetime.datetime.strptime("%s %s" % (splitted[1], splitted[2]), timestamp_format)
      fill_qty = float(splitted[7]) * 100
      _fill_price = float(splitted[9])
      result = True
    if result:
      if timestamp > self._time_current:
        self._state == ParserState.DONE
        return None
      self._time_last_fills[exchange_id] = timestamp
      self.add_turnover(timestamp, fill_qty)
    return result

  def parse_log_line(self, line: str):
    if self._use_fill_collector:  # Parse the log directly
      if self.handle_fill_collector(line):
        return
    else:  # Use csv fill dump
      if self.handle_fill_dump(line):
        return
    if self._state == ParserState.READY:
      datetime_parsed = self.detect_datetime(line)
      if datetime_parsed is not None:
        if datetime_parsed > self._time_current:
          self._state == ParserState.DONE
          return
        self._state = ParserState.HAS_DATETIME
      self._time = datetime_parsed
    elif self._state == ParserState.HAS_DATETIME:
      values = self.handle_price(line)
      if values is not None:
        value, value_1, value_2 = values
        self._state = ParserState.HAS_BALANCE
        self.update_values(value, value_1, value_2)
    elif self._state == ParserState.HAS_BALANCE:
      positions = self.handle_position(line)
      if positions is not None:
        position, position_1, position_2 = positions
        self._state = ParserState.READY
        self.update_positions(position, position_1, position_2)

  def get_result(self):
    last_fills = {}
    for exchange_id in (1, 2):
      if exchange_id in self._time_last_fills:
        last_fills[exchange_id] = (self._time_current - self._time_last_fills[exchange_id])

    mark_time = ''
    if self._time_marked:
      mark_time = '%s (%s)' % (format_time_marked(self._time_marked),
                               format_timedelta_short(
                                   datetime_diff_or_none(self._time_current, self._time_marked)))
    return {
        'name': self._name,
        'account_value': self._value_last,
        'mark_value': self._value_mark,
        'pnl_mark': datetime_diff_or_none(self._value_last, self._value_mark),
        'pnl_4h': datetime_diff_or_none(self._value_last, self._value_4h),
        'pnl_24h': datetime_diff_or_none(self._value_last, self._value_24h),
        'turnover_mark': self._turnover_mark,
        'turnover_4h': self._turnover_4h or None,
        'turnover_24h': self._turnover_24h or None,
        'balance_1': self._value_last_1 or None,
        'balance_2': self._value_last_2 or None,
        'position_1': self._position_last_1 or None,
        'position_2': self._position_last_2 or None,
        'last_fill_1': format_timedelta_long(last_fills.get(1)),
        'last_fill_2': format_timedelta_long(last_fills.get(2)),
        'mark_time': mark_time
    }

  def run(self, log_path, quote='BTC'):
    try:
      pos = find_log_pos_by_timestamp(log_path, self._time_begin)
    except FileNotFoundError:
      return self.get_result()

    with open(log_path, 'r', errors='ignore') as log_file:
      log_file.seek(pos)
      for line in log_file.readlines():
        self.parse_log_line(line)
        if self._state == ParserState.DONE:
          break
    return (quote, self.get_result())


def run_config(config: dict, current_time):
  name = config['name']
  log_path = config['log_path']
  use_fill_collector = config.get('fill_collector', 'csv') == 'fill_collector'
  balance_marker = config.get('balance_marker', 'BTC')
  position_marker = config.get('position_marker', 'USD')
  quote = config.get('quote', 'BTC')
  log_parser = LogParser(name,
                         current_time,
                         balance_marker=balance_marker,
                         position_marker=position_marker,
                         use_fill_collector=use_fill_collector)
  return log_parser.run(log_path, quote=quote)


def check_utc():
  timestamp = time.time()
  time_now = datetime.datetime.fromtimestamp(timestamp)
  time_utc = datetime.datetime.utcfromtimestamp(timestamp)
  return (time_now - time_utc).total_seconds() == 0


def report(mode, results):
  if mode == 'monitor':
    headers = [
        'name',
        'pnl_mark',
        'pnl_4h',
        'pnl_24h',
        'turnover_mark',
        'turnover_4h',
        'turnover_24h',
        'position_1',
        'position_2',
        'last_fill_1',
        'last_fill_2',
        'mark_time'
    ]
  elif mode == 'operation':
    headers = [
        'name',
        'account_value',
        'mark_value',
        'last_fill_1',
        'last_fill_2',
        'balance_1',
        'balance_2'
    ]
  elif mode == 'report':
    headers = ['name', 'pnl_mark', 'turnover_mark', 'mark_time']
  data = []
  pnl_mark_sum = 0
  for result in results:
    entry = []
    for header in headers:
      entry.append(result[header])
    data.append(entry)
    pnl_mark_sum += (result['pnl_mark'] or 0)
  print(tabulate.tabulate(data, headers=headers, floatfmt='.3f'))
  if 'pnl_mark' in headers:
    print(' * Total pnl_mark: %.3f' % pnl_mark_sum)


def main(argv):
  assert check_utc(), 'Invalid timezone'
  current_time = datetime.datetime.now()

  print('Time: %s' % current_time.strftime('%Y%m%d-%H%M%S'))

  config = {
      'name': 'pass-model.bitmex-xbtusd.03',
      'log_path': ('/home/ec2-user/data/log/'
                   'strategy-10.eu-west-1/log_pass_model_0'),
      'fill_collector': 'fill_collector',
      'balance_marker': 'Total',
      'position_marker': 'BTC-USD.PERPETUAL.Bitmex.XBTUSD',
  }

  run_config(config, current_time)


if __name__ == '__main__':
  logging.basicConfig(level='DEBUG', format='%(levelname)8s %(asctime)s %(name)s] %(message)s')
  try:
    sys.exit(main(sys.argv))
  except KeyboardInterrupt:
    sys.exit(1)
